package org.dding.msc.service.listener;

import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.dding.msc.exception.ServiceLifecycException;
import org.dding.msc.service.ServiceController;
import org.dding.msc.service.ServiceListener;
import org.dding.msc.service.ServiceName;

/**
 * Service listener that allow certain behaviors to be expected. Like services
 * starting, stopping, etc. The expect calls return a
 * {@code java.util.concurrent.Future} that can be used to retrieve the
 * controller once the expected behavior has occurred.
 * 
 */
public class ProbeServiceStateServiceListener implements
		ServiceListener<Object> {
	private final ConcurrentHashMap<ServiceName, ServiceFutureWithValidation> expectedServiceStops = new ConcurrentHashMap<ServiceName, ServiceFutureWithValidation>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedRemovalRequests = new ConcurrentHashMap<ServiceName, ServiceFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFailureFuture> expectedServiceProblems = new ConcurrentHashMap<ServiceName, ServiceFailureFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedServiceAvailabilities = new ConcurrentHashMap<ServiceName, ServiceFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedServiceUnavailabilities = new ConcurrentHashMap<ServiceName, ServiceFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedTransitiveDepAvailabilities = new ConcurrentHashMap<ServiceName, ServiceFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedTransitiveDepUnavailabilities = new ConcurrentHashMap<ServiceName, ServiceFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedServiceWaitings = new ConcurrentHashMap<ServiceName, ServiceFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedServiceStarts = new ConcurrentHashMap<ServiceName, ServiceFuture>();
	private final ConcurrentHashMap<ServiceName, ServiceFuture> expectedListenerAddeds = new ConcurrentHashMap<ServiceName, ServiceFuture>();

	private final EnumMap<ServiceController.Transition, Map<ServiceName, ServiceFuture>> expectedTransitions = new EnumMap<ServiceController.Transition, Map<ServiceName, ServiceFuture>>(
			ServiceController.Transition.class);

	public ProbeServiceStateServiceListener() {
		for (ServiceController.Transition transition : ServiceController.Transition
				.values()) {
			expectedTransitions.put(transition,
					new HashMap<ServiceName, ServiceFuture>());
		}
	}

	public void listenerAdded(
			final ServiceController<? extends Object> serviceController) {
		final ServiceFuture future = expectedListenerAddeds
				.remove(serviceController.getName());
		if (future != null) {
			future.setServiceController(serviceController);
		}
	}

	public void transition(
			final ServiceController<? extends Object> serviceController,
			final ServiceController.Transition transition) {
		switch (transition) {
		
		case STOPPING_to_PROBLEM:
		case STARTING_to_PROBLEM: {
			final ServiceFailureFuture future = expectedServiceProblems
					.remove(serviceController.getName());
			if (future != null) {
				future.setServiceLifecycException(serviceController.getServiceLifecycException());
			}
			break;
		}
		case STOPPING_to_DOWN: {
			final ServiceFuture future = expectedTransitions.get(transition)
					.remove(serviceController.getName());
			expectedServiceStops.remove(serviceController.getName());
			if (future != null) {
				future.setServiceController(serviceController);
			}
			break;
		}
		default: {
			final ServiceFuture future = expectedTransitions.get(transition)
					.remove(serviceController.getName());
			if (future != null) {
				future.setServiceController(serviceController);
			}
			break;
		}
		}
	}

	public void serviceRemoveRequested(
			final ServiceController<? extends Object> serviceController) {
		final ServiceFuture future = expectedRemovalRequests
				.remove(serviceController.getName());
		if (future != null) {
			future.setServiceController(serviceController);
		}
	}

	public void serviceAvailable(
			ServiceController<? extends Object> serviceController) {
		final ServiceFuture future = expectedServiceAvailabilities
				.remove(serviceController.getName());
		if (future != null) {
			future.setServiceController(serviceController);
		}
	}

	public void serviceUnavailable(
			ServiceController<? extends Object> serviceController) {
		final ServiceFuture future = expectedServiceUnavailabilities
				.remove(serviceController.getName());
		if (future != null) {
			future.setServiceController(serviceController);
		}
	}

	public void transitiveServiceAvailable(
			ServiceController<? extends Object> serviceController) {
		final ServiceFuture future = expectedTransitiveDepAvailabilities
				.remove(serviceController.getName());
		if (future != null) {
			future.setServiceController(serviceController);
		}
	}

	public void transitiveServiceUnavailable(
			ServiceController<? extends Object> serviceController) {
		final ServiceFuture future = expectedTransitiveDepUnavailabilities
				.remove(serviceController.getName());
		if (future != null) {
			future.setServiceController(serviceController);
		}
	}

	// ----

	public Future<ServiceController<?>> expectListenerAdded(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture();
		expectedListenerAddeds.put(serviceName, future);
		return future;
	}

	public Future<ServiceController<?>> expectNoListenerAdded(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture(100);
		expectedListenerAddeds.put(serviceName, future);
		return future;
	}

	public Future<ServiceController<?>> expectServiceStart(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture();
		expectedTransitions.get(ServiceController.Transition.STARTING_to_UP)
				.put(serviceName, future);
		return future;
	}

	public Future<ServiceController<?>> expectServiceStop(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture();
		expectedTransitions.get(ServiceController.Transition.UP_to_STOPPING)
				.put(serviceName, future);
		return future;
	}

	public Future<ServiceController<?>> expectServiceDown(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture();
		expectedTransitions.get(
				ServiceController.Transition.STOPPING_to_DOWN).put(
				serviceName, future);
		return future;
	}

	public Future<ServiceController<?>> expectServiceWaiting(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture();
		expectedTransitions.get(ServiceController.Transition.UP_to_WAITING)
				.put(serviceName, future);
		return future;
	}

	public Future<ServiceController<?>> expectServiceWaitingCleared(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture();
		expectedTransitions.get(ServiceController.Transition.WAITING_to_STOPPING)
				.put(serviceName, future);
		return future;
	}

	public Future<ServiceController<?>> expectServiceWontStart(
			final ServiceName serviceName) {
		final ServiceFuture future = new ServiceFuture();
		expectedTransitions
				.get(ServiceController.Transition.STOPPING_to_PROBLEM).put(
						serviceName, future);
		return future;
	}

//	public Future<ServiceController<?>> expectServiceWontStartCleared(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedTransitions
//				.get(ServiceController.Transition.p).put(
//						serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectServiceRemovalRequest(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedRemovalRequests.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoServiceRemovalRequest(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedRemovalRequests.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectServiceRemovalRequestCleared(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedRemovalRequestClears.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectServiceRemoval(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedTransitions.get(
//				ServiceController.Transition.REMOVING_to_REMOVED).put(
//				serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoServiceRemoval(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedTransitions.get(
//				ServiceController.Transition.REMOVING_to_REMOVED).put(
//				serviceName, future);
//		return future;
//	}
//
//	public Future<StartException> expectServiceFailure(
//			final ServiceName serviceName) {
//		final ServiceFailureFuture future = new ServiceFailureFuture();
//		expectedFailures.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectDependencyFailure(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedDependencyFailures.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoDependencyFailure(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedDependencyFailures.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectDependencyFailureCleared(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedDependencyRetryings.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoDependencyFailureCleared(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedDependencyRetryings.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectImmediateDependencyAvailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedImmediateDepAvailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoImmediateDependencyAvailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedImmediateDepAvailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectImmediateDependencyUnavailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedImmediateDepUnavailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoImmediateDependencyUnavailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedImmediateDepUnavailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectTransitiveDependencyAvailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedTransitiveDepAvailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoTransitiveDependencyAvailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedTransitiveDepAvailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectTransitiveDependencyUnavailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedTransitiveDepUnavailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoTransitiveDependencyUnavailable(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedTransitiveDepUnavailabilities.put(serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectDependencyProblem(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedTransitions.get(
//				ServiceController.Transition.START_REQUESTED_to_PROBLEM).put(
//				serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoDependencyProblem(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedTransitions.get(
//				ServiceController.Transition.START_REQUESTED_to_PROBLEM).put(
//				serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectDependencyProblemCleared(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture();
//		expectedTransitions.get(
//				ServiceController.Transition.PROBLEM_to_START_REQUESTED).put(
//				serviceName, future);
//		return future;
//	}
//
//	public Future<ServiceController<?>> expectNoDependencyProblemCleared(
//			final ServiceName serviceName) {
//		final ServiceFuture future = new ServiceFuture(100);
//		expectedTransitions.get(
//				ServiceController.Transition.PROBLEM_to_START_REQUESTED).put(
//				serviceName, future);
//		return future;
//	}

	private static class ServiceFuture implements Future<ServiceController<?>> {
		private ServiceController<?> serviceController;
		private final CountDownLatch countDownLatch = new CountDownLatch(1);
		private final long delay;
		private final TimeUnit delayTimeUnit;

		ServiceFuture() {
			delay = 500L;
			delayTimeUnit = TimeUnit.MILLISECONDS;
		}

		ServiceFuture(long timeInMs) {
			delay = timeInMs;
			delayTimeUnit = TimeUnit.MILLISECONDS;
		}

		@Override
		public boolean cancel(boolean mayInterruptIfRunning) {
			return false;
		}

		@Override
		public boolean isCancelled() {
			return false;
		}

		@Override
		public boolean isDone() {
			return serviceController != null;
		}

		@Override
		public ServiceController<?> get() throws InterruptedException,
				ExecutionException {
			try {
				return get(delay, delayTimeUnit);
			} catch (TimeoutException e) {
				throw new RuntimeException("Could not get start exception in "
						+ delay + " " + delayTimeUnit + " timeout.");
			}
		}

		@Override
		public ServiceController<?> get(long timeout, TimeUnit unit)
				throws InterruptedException, ExecutionException,
				TimeoutException {
			countDownLatch.await(timeout, unit);
			return serviceController;
		}

		private void setServiceController(
				final ServiceController<?> serviceController) {
			this.serviceController = serviceController;
			countDownLatch.countDown();
		}
	}

	private static class ServiceFutureWithValidation extends ServiceFuture {
		private boolean valid = true;

		ServiceFutureWithValidation() {
			super();
		}

		public void invalidate() {
			valid = false;
		}

		@Override
		public ServiceController<?> get(long timeout, TimeUnit unit)
				throws InterruptedException, ExecutionException,
				TimeoutException {
			ServiceController<?> result = super.get(timeout, unit);
			return valid ? result : null;
		}
	}

	private static class ServiceFailureFuture implements Future<ServiceLifecycException> {
		private ServiceLifecycException startException;
		private final CountDownLatch countDownLatch = new CountDownLatch(1);

		@Override
		public boolean cancel(boolean mayInterruptIfRunning) {
			return false;
		}

		@Override
		public boolean isCancelled() {
			return false;
		}

		@Override
		public boolean isDone() {
			return startException != null;
		}

		@Override
		public ServiceLifecycException get() throws InterruptedException,
				ExecutionException {
			try {
				return get(30L, TimeUnit.SECONDS);
			} catch (TimeoutException e) {
				throw new RuntimeException(
						"Could not get start exception in 30 second timeout.");
			}
		}

		@Override
		public ServiceLifecycException get(long timeout, TimeUnit unit)
				throws InterruptedException, ExecutionException,
				TimeoutException {
			countDownLatch.await(timeout, unit);
			return startException;
		}

		public void setServiceLifecycException(ServiceLifecycException startException) {
			this.startException = startException;
			countDownLatch.countDown();
		}
	}
}
